// this code is heavily copied :)

using System;
using System.Text;
using System.IO.Ports;
using Microsoft.Ccr.Core;

using generici2c = Generic.Protocols.I2C.Proxy;

namespace Devantech.Services.I2C.Hardware
{
    class SerialOperations : PortSet<I2CSend, I2CRecv, I2CShutdown> { }

    class I2CSend
    {
        public byte[] Data;
        public SuccessFailurePort ResponsePort = new SuccessFailurePort();
    }

    // a empty notification that data exists on the serial port
    class I2CRecv { }

    // a shutdown message
    class I2CShutdown { }

    class ResponsePort<T> : PortSet<T,Exception> { }

    class I2CHardwarePacket : CcrServiceBase, IDisposable
    {
        private const byte CM02_I2C_CMD = 0x55;
        private const byte CM02_CM02_CMD = 0x5A;
        SerialPort _port;
        SerialOperations _operationsPort = new SerialOperations();
        Port<byte> _dataPort = new Port<byte>();
        Port<bool> _sendLock = new Port<bool>();

        #region Service Internals
        public I2CHardwarePacket(DispatcherQueue queue, string portName, int speed, Parity parity, int dataBits, StopBits stopBits)
            : base(queue)
        {
            Activate(
                Arbiter.Interleave(
                    new TeardownReceiverGroup
                    (
                        Arbiter.Receive<I2CShutdown>(false, _operationsPort, ShutdownHandler)
                    ),
                    new ExclusiveReceiverGroup
                    (
                        Arbiter.JoinedReceive<I2CSend, bool>(true, _operationsPort, _sendLock, SendHandler),
                        Arbiter.Receive<I2CRecv>(true, _operationsPort, RecvHandler)
                    ),
                    new ConcurrentReceiverGroup
                    (
                    )
                )
            );

            _port = new SerialPort(
                portName,
                speed,
                parity,
                dataBits,
                stopBits // one for RF04, two for USB I2C
            );

            _port.WriteTimeout = 100;
            _port.ReadTimeout = 100;
            _port.DataReceived += _port_DataReceived;

            _port.Open();

            Console.Out.WriteLine("Port " + portName + " opened.");
            _sendLock.Post(true);
        }

        // serial port incoming byte event handler
        void _port_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            _operationsPort.Post(new I2CRecv());
        }

        void ShutdownHandler(I2CShutdown shutdown)
        {
            if (_port != null &&
                _port.IsOpen)
            {
                _port.DataReceived -= _port_DataReceived;
                _port.Close();
                _port = null;
            }
        }

        public void Close()
        {
            _operationsPort.Post(new I2CShutdown());
        }

        void SendHandler(I2CSend send, bool sendLock)
        {
            try
            {
                if (_dataPort.ItemCount > 0)
                {
                    byte temp;
                    StringBuilder builder = new StringBuilder();
                    builder.AppendFormat("Sending. {0} bytes pulled from channel: ", _dataPort.ItemCount);
                    while (_dataPort.Test(out temp))
                    {
                        builder.AppendFormat("{0:X2} ", temp);
                    }
                
                }

                _port.DiscardInBuffer();
                _port.Write(send.Data, 0, send.Data.Length);
             
                send.ResponsePort.Post(new SuccessResult());
            }
            catch (Exception e)
            {
                send.ResponsePort.Post(new Exception("Send Exception: " + e.ToString()));
            }
           
        }

        private string PrintBytes(byte[] bytes)
        {
            StringBuilder sb = new StringBuilder();
            foreach (byte b in bytes)
            {
                sb.Append(b.ToString() + " ");
            }
            return sb.ToString();
        }

        void _port_PinChanged(object sender, SerialPinChangedEventArgs e)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        void RecvHandler(I2CRecv recv)
        {
           // Console.Out.WriteLine("Receive handler");
            while (_port.BytesToRead > 0)
            {
              //  Console.Out.WriteLine("Received byte");
                _dataPort.Post((byte)_port.ReadByte());
            }
        }

        delegate T RecvDataHandler<T>(params byte[] data);

        ResponsePort<T> SendAndRecv<T>(SuccessFailurePort sendPort, int recvSize, RecvDataHandler<T> recvHandler)
        {
            ResponsePort<T> result = new ResponsePort<T>();

            Activate(
                Arbiter.Choice(
                    sendPort,
                    delegate(SuccessResult success)
                    {
                        Activate(
                            Arbiter.Choice(
                                Arbiter.MultipleItemReceive<byte>(false, _dataPort, recvSize,
                                    delegate(byte[] data)
                                    {
                                        result.Post(recvHandler(data));
                                        ResetSendLock(true);
                                    }
                                ),
                                Arbiter.Receive(false, TimeoutPort(100 * recvSize),
                                    delegate(DateTime time)
                                    {
                                        byte temp;
                                        StringBuilder builder = new StringBuilder();
                                        builder.AppendFormat("Reading {0} bytes timed out. {1} pulled from channel: ", recvSize, _dataPort.ItemCount);
                                        while (_dataPort.Test(out temp))
                                        {
                                            builder.AppendFormat("{0:X2} ", temp);
                                        }
                                        result.Post(new TimeoutException(builder.ToString()));
                                        ResetSendLock(false);
                                    }
                                )
                            )
                        );
                    },
                    delegate(Exception e)
                    {
                        result.Post(new Exception(" sendandreceiveexception " + e.ToString()));
                        ResetSendLock(false);
                    }
                )
            );
            return result;
        }

        private void ResetSendLock(bool success)
        {
            Activate(
                Arbiter.Receive(false, TimeoutPort(100),
                    delegate(DateTime time)
                    {
                        _sendLock.Post(success);
                    }
                )
            );
        }
        #endregion

        /// <summary>
        /// Does a read data request to the I2C bus
        /// </summary>
        /// <param name="readDataRequest">The read data request.</param>
        /// <returns></returns>
        public ResponsePort<byte[]> ReadDataAction(generici2c.ReadDataRequest readDataRequest)
        {
            byte[] sendData = new byte[4];
            sendData[0] = CM02_I2C_CMD;
            sendData[1] = (byte)(readDataRequest.I2CAddress + 1); // a read
            sendData[2] = readDataRequest.RegisterAddress;
            sendData[3] = (byte)readDataRequest.NumBytes;

            return SendAndRecv<byte[]>(
                Send(sendData),
                readDataRequest.NumBytes,
                delegate(byte[] reply)
                {
                    return reply;
                }
            );
        }

        /// <summary>
        /// Does a write data request to the I2C bus
        /// </summary>
        /// <param name="writeDataRequest">The write data request.</param>
        /// <returns></returns>
        public ResponsePort<byte[]> WriteDataAction(generici2c.WriteDataRequest writeDataRequest)
        {
            byte[] sendData = new byte[writeDataRequest.Data.Length + 4];
            sendData[0] = CM02_I2C_CMD;
            sendData[1] = (byte)(writeDataRequest.I2CAddress);
            sendData[2] = writeDataRequest.RegisterAddress;
            sendData[3] = (byte)writeDataRequest.Data.Length;
            if (writeDataRequest.Data.Length > 0)
            {
                writeDataRequest.Data.CopyTo(sendData, 4);
            }
            
            return SendAndRecv<byte[]>(
               Send(sendData),
               1,
               delegate(byte[] reply)
               {
                   return reply;
               }
           );
        }

        /// <summary>
        /// A Read request to the Devantech CM02 internal registers
        /// </summary>
        /// <param name="devantechReadDataRequest">The devantech read data request.</param>
        /// <returns>The response as a byte[]</returns>
        public ResponsePort<byte[]> DevantechReadDataAction(DevantechReadDataRequest devantechReadDataRequest)
        {
            // Console.Out.WriteLine("Packet CM02 Read data action called");
            byte[] sendData = new byte[4];
            sendData[0] = CM02_CM02_CMD;
            sendData[1] = devantechReadDataRequest.Command;

            // fill missing data with zeros to get a four byte packet.
            switch (devantechReadDataRequest.Data.Length)
            {
                case (0):
                    sendData[2] = (byte)0;
                    sendData[3] = (byte)0;
                    break;
                case (1):
                    sendData[2] = devantechReadDataRequest.Data[0];
                    sendData[3] = (byte)0;
                    break;
                case (2):
                    sendData[2] = devantechReadDataRequest.Data[0];
                    sendData[3] = devantechReadDataRequest.Data[1];
                    break;
                default:
                    sendData[2] = (byte)0;
                    sendData[3] = (byte)0;
                    break;
            }

            return SendAndRecv<byte[]>(
                Send(sendData),
                devantechReadDataRequest.NumBytes,
                delegate(byte[] reply)
                {
                    return reply;
                }
            );
        }


        /// <summary>
        /// Sends the specified command.
        /// </summary>
        /// <param name="command">The command.</param>
        /// <returns></returns>
        private SuccessFailurePort Send(byte[] command)
        {
            I2CSend send = new I2CSend();
            send.Data = command;
            _operationsPort.Post(send);
            return send.ResponsePort;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            // close the serial port
            _port.Close();
            _port = null;
        }
    }
}
